﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Xom.Generator.Templates;

namespace Xom.Generator
{
    public static class BindingGenerator
    {
        private static readonly IList<XsdDocument> XsdFiles = new List<XsdDocument>();
        /// <summary>
        /// Genreate Interface and Class definitions in one file.
        /// </summary>
        private static readonly bool _allInOneFile = false;

        public static IList<string> Load(IList<Stream> xsdFiles, string nameSpace)
        {
            XsdFiles.Clear();

            foreach (Stream file in xsdFiles)
            {
                XsdDocument xsd = new XsdDocument(XDocument.Load(file), "");
                XsdFiles.Add(xsd);
            }

            //read elements
            ReadSchemaElements();

            //genreate c# code
            GenerateCode(nameSpace);

            //return results
            return CreateCodeStrings();
        }

        public static void Load(IList<string> xsdFiles, string nameSpace, string directory)
        {
            XsdFiles.Clear();

            foreach (string fileName in xsdFiles)
            {
                XDocument document = XDocument.Load(fileName, LoadOptions.SetBaseUri);
                XsdFiles.Add(new XsdDocument(document, document.BaseUri));
            }

            //read elements
            ReadSchemaElements();

            //genreate c# code
            GenerateCode(nameSpace);

            //create files
            CreateCodeFiles(directory);
        }

        public static void Load(IList<XDocument> xsdFiles, string nameSpace, string directory)
        {
            XsdFiles.Clear();

            foreach (XDocument document in xsdFiles)
            {
                XsdFiles.Add(new XsdDocument(document, document.BaseUri));
            }

            //read elements
            ReadSchemaElements();

            //genreate c# code
            GenerateCode(nameSpace);

            //create files
            CreateCodeFiles(directory);
        }

        private static void ReadSchemaElements()
        {
            foreach (XsdDocument document in XsdFiles)
            {
                //Import NameSpace -> needed for XElement names
                foreach (XAttribute attribute in document.XsdFile.Root.Attributes())
                {
                    document.NameSpaces.Add(attribute.Name.LocalName, attribute.Value);
                }

                foreach (XElement element in document.XsdFile.Root.Elements())
                {
                    if (element.Name.LocalName.ToLower() == "include") continue;

                    CreateXsdType(document, element, GetDescription(element));
                }
            }

            //basic elements done -> now read detail informations
            ReadAllComplexeTypesDetails();
        }

        private static string GetDescription(XElement element)
        {
            string description = string.Empty;
            if (element.PreviousNode == null) return description;
            if (element.PreviousNode.GetType() == typeof(XComment))
            {
                description = ((XComment)element.PreviousNode).Value.Trim();
                if (string.IsNullOrEmpty(description) || string.IsNullOrWhiteSpace(description))
                {
                    if (element.PreviousNode.PreviousNode != null && element.PreviousNode.PreviousNode.GetType() == typeof(XComment))
                    {
                        description = ((XComment)element.PreviousNode.PreviousNode).Value.Trim();
                    }
                }
            }

            if (!string.IsNullOrEmpty(description))
            {
                description = description.Replace("<", "");
                description = description.Replace(">", "");
            }

            return description;
        }

        private static string CreateXsdType(XsdDocument document, XElement element, string description)
        {
            switch (element.Name.LocalName.ToLower())
            {
                case "simpletype":
                    {
                        return ReadSimpleTypes(document, element);
                    }
                case "element":
                    {
                        return ReadComplexeTypes(document, element, description);
                    }
                case "complextype":
                    {
                         return ReadComplexeTypes(document, element, description);
                    }
            }

            return string.Empty;
        }

        private static string ReadSimpleTypes(XsdDocument document, XElement element)
        {
            XsdSimpleType newType = new XsdSimpleType(element.Attribute("name").Value);
            newType.XsdNode = element;

            XElement restriction = element.Element("{" + document.NameSpaces["xs"] + "}restriction");
            if (restriction != null)
            {
                string baseValue = restriction.Attribute("base").Value;
                if (baseValue.StartsWith("xs:"))
                {
                    newType.TypeName = XsdHelper.GetSimpleBaseType(baseValue, false);
                }

                foreach (XElement child in restriction.Elements())
                {
                    if (child.Name.LocalName == "enumeration")
                    {
                        if (!document.EnumerationTypes.ContainsKey(newType.XmlName))
                        {
                            XsdEnumerationType enumType = new XsdEnumerationType(newType.Name+"Enum");
                            enumType.XsdNode = restriction;
                            document.EnumerationTypes.Add(newType.XmlName, enumType);
                        }

                        document.EnumerationTypes[newType.XmlName].Enumerations.Add(child.Attribute("value").Value);
                    }
                }
            }

            XElement union = element.Element("{" + document.NameSpaces["xs"] + "}union");
            if (union != null)
            {
                string memberTypes = union.Attribute("memberTypes").Value;
                string[] members = memberTypes.Split(' ');

                foreach (string member in members)
                {
                    if (document.SimpleTypes.ContainsKey(member))
                    {
                        newType.TypeName = document.SimpleTypes[member].TypeName;
                        break;
                    }
                }
            }

            document.SimpleTypes.Add(newType.XmlName, newType);
            return newType.XmlName;
        }

        private static string ReadComplexeTypes(XsdDocument document, XElement element, string description)
        {
            XsdComplexType newType = new XsdComplexType(element.Attribute("name").Value);
            newType.Description = description;

            XElement complexNode = element;
            if (complexNode.Name.LocalName.ToLower() == "element")
            {
                //inner typ definition
                if (complexNode.HasElements)
                {
                    complexNode = element.Element("{" + document.NameSpaces["xs"] + "}complexType");
                }
                //external typ definition
                else
                {
                    XAttribute typeName = complexNode.Attribute("type");
                    if (document.ComplexTypes.ContainsKey(typeName.Value))
                    {
                        document.ComplexTypes[typeName.Value].Name = newType.Name;
                        return newType.XmlName;
                    }
                }
            }

            if (complexNode == null) return string.Empty;
            newType.XsdNode = complexNode;

            document.ComplexTypes.Add(newType.XmlName, newType);
            return newType.XmlName;
        }

        private static XsdTypeReference ReadAttributeElement(XsdDocument document, XElement element, int index)
        {
            XAttribute name = element.Attribute("name");
            XAttribute use = element.Attribute("use");
            bool isRequired = use != null && use.Value == "required";

            XsdTypeReference reference = new XsdTypeReference(name.Value, true, isRequired, false, index);
            GetElementType(document, element, reference);

            return reference;
        }

        private static void ReadAllComplexeTypesDetails()
        {
            foreach (XsdDocument document in XsdFiles)
            {
                //create foreach list because inner types might be added at runtime
                IList<XsdComplexType> complexTypes = document.ComplexTypes.Values.ToList();

                foreach (XsdComplexType xsdComplexType in complexTypes)
                {
                    ReadComplexeTypesDetails(document, xsdComplexType);
                }
            }
        }

        private static void ReadComplexeTypesDetails(XsdDocument document, XsdComplexType xsdComplexType)
        {
            int attributeIndex = 1000;

            foreach (XElement element in xsdComplexType.XsdNode.Elements())
            {
                string elementType = element.Name.LocalName.ToLower();

                switch (elementType)
                {
                    case "simplecontent":
                        {
                            //read extension base 
                            XElement extention = element.Element("{" + document.NameSpaces["xs"] + "}extension");
                            XsdTypeReference content = new XsdTypeReference("Value", false, false, false, 0);
                            content.Description = GetDescription(element);
                            content.XmlName = "";
                            GetElementType(document, extention, content);

                            xsdComplexType.Properties.Add(content.XmlName, content);

                            int index = 1000;
                            //read attributes
                            foreach (XElement attribute in extention.Elements())
                            {
                                if (attribute.Name.LocalName.ToLower() == "attribute")
                                {
                                    XsdTypeReference reference = ReadAttributeElement(document, attribute, index);
                                    xsdComplexType.Properties.Add(reference.XmlName, reference);

                                    index = index + 1000;
                                }
                            }

                            break;
                        }
                    case "complexcontent":
                        {
                            XElement restriction = element.Element("{" + document.NameSpaces["xs"] + "}restriction");
                            if (restriction != null)
                            {
                                foreach (XElement attribute in restriction.Elements("{" + document.NameSpaces["xs"] + "}attribute"))
                                {
                                    XsdTypeReference reference = ReadAttributeElement(document, attribute, attributeIndex);
                                    reference.Description = GetDescription(attribute);
                                    xsdComplexType.Properties.Add(reference.XmlName, reference);

                                    attributeIndex = attributeIndex + 1000;
                                }
                            }

                            break;
                        }
                    case "attribute":
                        {
                            XsdTypeReference reference = ReadAttributeElement(document, element, attributeIndex);
                            reference.Description = GetDescription(element);
                            xsdComplexType.Properties.Add(reference.XmlName, reference);

                            attributeIndex = attributeIndex + 1000;
                            break;
                        }
                    case "choice":
                        {
                            ReadSequenceElements(document, element, xsdComplexType, true,10);
                            break;
                        }
                    case "sequence":
                        {
                            ReadSequenceElements(document, element, xsdComplexType, false,10);
                            break;
                        }
                    case "all":
                        {
                            ReadSequenceElements(document, element, xsdComplexType, true,10);
                            break;
                        }
                }
            }
        }

        private static int ReadSequenceElements(XsdDocument document, XElement sequence, XsdComplexType complexType, bool allOptional, int index)
        {
            foreach (XElement element in sequence.Elements())
            {
                switch (element.Name.LocalName)
                {
                    case "element":
                        {
                            XAttribute name = element.Attribute("name");
                            if (name == null)
                            {
                                name = element.Attribute("ref");
                                if (name == null) continue;
                            }
                            XAttribute minOccurs = element.Attribute("minOccurs");
                            XAttribute maxOccurs = element.Attribute("maxOccurs");
                            bool isRequired = true;
                            bool isList = false;

                            if (minOccurs != null && Convert.ToUInt32(minOccurs.Value) == 0)
                            {
                                isRequired = false;
                            }
                            if (maxOccurs != null &&
                                (maxOccurs.Value == "unbounded" || Convert.ToUInt32(maxOccurs.Value) > 1))
                            {
                                isList = true;
                            }

                            //"choise" and "all" elements are optional
                            if (allOptional) isRequired = false;

                            XsdTypeReference reference = new XsdTypeReference(name.Value, false, isRequired, isList,
                                                                              index);
                            reference.Description = GetDescription(element);
                            GetElementType(document, element, reference);

                            complexType.Properties.Add(reference.XmlName, reference);
                            index = index + 10;
                            break;
                        }
                    case "choice":
                        {
                            index = ReadSequenceElements(document, element, complexType, true, index);
                            break;
                        }
                    case "sequence":
                        {
                            index = ReadSequenceElements(document, element, complexType, false, index);
                            break;
                        }
                }
            }

            return index;
        }

        private static void GetElementType(XsdDocument document, XElement element, XsdTypeReference reference)
        {
            XAttribute type = element.Attribute("type");
            reference.IsSimpleType = false;

            if (type == null)
            {
                XAttribute refElement = element.Attribute("ref");
                if (refElement != null)
                {
                    SetReferenceType(refElement.Value, reference);
                }
                else
                {
                    XAttribute baseElement = element.Attribute("base");
                    if (baseElement != null)
                    {
                        SetReferenceType(baseElement.Value, reference);
                    }
                    else
                    {
                        //create new type
                        string newTypeName = CreateXsdType(document, element, GetDescription(element));
                        if (document.ComplexTypes.ContainsKey(newTypeName))
                        {
                            ReadComplexeTypesDetails(document, document.ComplexTypes[newTypeName]);
                        }

                        SetReferenceType(newTypeName, reference);
                    }
                }
            }
            else
            {
                SetReferenceType(type.Value, reference);
            }
        }

        private static void SetReferenceType(string type, XsdTypeReference reference)
        {
            if (type.StartsWith("xs:"))
            {
                reference.TypeInterfaceName = XsdHelper.GetSimpleBaseType(type, reference.IsRequired);
                reference.TypeName = XsdHelper.GetSimpleBaseType(type, reference.IsRequired);
                reference.IsSimpleType = true;
            }
            else
            {
                foreach (XsdDocument document in XsdFiles)
                {
                    if (document.ComplexTypes.ContainsKey(type))
                    {
                        reference.TypeInterfaceName = document.ComplexTypes[type].InterfaceName;
                        reference.TypeName = document.ComplexTypes[type].Name;
                    }
                    if (document.SimpleTypes.ContainsKey(type))
                    {
                        reference.TypeInterfaceName = document.SimpleTypes[type].TypeName;
                        reference.TypeName = document.SimpleTypes[type].TypeName;
                        reference.IsSimpleType = true;
                    }
                    if (document.EnumerationTypes.ContainsKey(type))
                    {
                        reference.TypeInterfaceName = document.EnumerationTypes[type].Name;
                        reference.TypeName = document.EnumerationTypes[type].Name;
                        reference.IsSimpleType = true;

                        if (!reference.IsRequired)
                        {
                            reference.TypeInterfaceName = reference.TypeInterfaceName + "?";
                            reference.TypeName = reference.TypeName + "?";
                        }
                    }
                }
            }

            //if type not appears in the document list -> insert name/interface from value
            if (string.IsNullOrEmpty(reference.TypeName))
            {
                string typeName = XsdHelper.FilterName(type);
                reference.TypeInterfaceName = "I" + typeName;
                reference.TypeName = typeName;
            }
        }

        private static IList<string> CreateCodeStrings()
        {
            return XsdFiles.Select(document => document.Code).ToList();
        }

        private static void CreateCodeFiles(string directory)
        {
            foreach (XsdDocument document in XsdFiles)
            {
                string name = document.Name + ".cs";
                File.WriteAllText(Path.Combine(directory, name), document.Code);
                //File.WriteAllText(Path.Combine(directory, "I" + name), document.InterfaceCode);
            }
        }

        private static void GenerateCode(string nameSpace)
        {
            foreach (XsdDocument document in XsdFiles)
            {
                IList<XsdComplexType> complexTypes = document.ComplexTypes.Values.ToList();
                IList<XsdEnumerationType> enumTypes = document.EnumerationTypes.Values.ToList();

                FileTemplate template = new FileTemplate(complexTypes, enumTypes, nameSpace);
                document.Code = template.TransformText();
            }
        }
    }

    
}
